default_platform :ios
opt_out_usage
skip_docs

require 'json'
require 'net/http'
import 'Sonarfile'
import 'Allurefile'

xcode_version = ENV['XCODE_VERSION'] || '16.4'
xcode_project = 'StreamChat.xcodeproj'
sdk_names = ['StreamChat', 'StreamChatUI']
github_repo = ENV['GITHUB_REPOSITORY'] || 'GetStream/stream-chat-swift'
stress_tests_cycles = 50
derived_data_path = 'derived_data'
source_packages_path = 'spm_cache'
metrics_git = 'git@github.com:GetStream/stream-internal-metrics.git'
xcmetrics_path = "metrics/#{github_repo.split('/').last}-xcmetrics.json"
buildcache_xcargs = 'CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++'
testlab_bucket = 'gs://test-lab-af3rt9m4yh360-mqm1zzm767nhc'
swift_environment_path = File.absolute_path('../Sources/StreamChat/Generated/SystemEnvironment+Version.swift')
is_localhost = !is_ci
@force_check = false

before_all do |lane|
  if is_ci
    setup_ci
    setup_git_config
    select_xcode(version: xcode_version) unless [:sonar_upload, :allure_launch, :allure_upload, :pod_lint, :sync_mock_server, :copyright, :merge_main].include?(lane)
  end
end

after_all do |lane|
  stop_sinatra if lane == :test_e2e_mock
end

desc "Build .xcframeworks"
lane :build_xcframeworks do
  match_me
  output_directory = "#{Dir.pwd}/../Products"
  team_id = File.read('Matchfile').match(/team_id\("(.*)"\)/)[1]
  codesign = ["codesign --timestamp -v --sign 'Apple Distribution: Stream.io Inc (#{team_id})'"]
  sdk_names.each do |sdk|
    create_xcframework(
      project: xcode_project,
      scheme: sdk,
      destinations: ['iOS'],
      include_BCSymbolMaps: true,
      include_debug_symbols: true,
      xcframework_output_directory: output_directory,
      remove_xcarchives: true
    )
    sh('../Scripts/removeUnneededSymbols.sh', sdk, output_directory)
    codesign << lane_context[SharedValues::XCFRAMEWORK_OUTPUT_PATH]
    resolve_swiftui_xcframework_issue if sdk == 'StreamChatUI'
  end
  sh(codesign.join(' ')) # We need to sign all frameworks at once
end

desc 'https://linear.app/stream/issue/IOS-630'
private_lane :resolve_swiftui_xcframework_issue do
  Dir.glob("#{lane_context[SharedValues::XCFRAMEWORK_OUTPUT_PATH]}/**/*.swiftinterface").each do |file|
    old_text = File.read(file)
    new_text = old_text.gsub(/SwiftUICore.View/, 'View')
    File.open(file, 'w') { |f| f.puts(new_text) } if old_text != new_text
  end
end

desc 'Start a new release'
lane :release do |options|
  artifacts_path = File.absolute_path('../StreamChatArtifacts.json')
  extra_changes = lambda do |release_version|
    # Set the framework version on the artifacts
    artifacts = JSON.parse(File.read(artifacts_path))
    artifacts[release_version.to_s] = "https://github.com/#{github_repo}/releases/download/#{release_version}/StreamChat-All.zip"
    File.write(artifacts_path, JSON.dump(artifacts))

    # Set the framework version in SystemEnvironment+Version.swift
    old_content = File.read(swift_environment_path)
    current_version = old_content[/version: String = "([^"]+)"/, 1]
    new_content = old_content.gsub(current_version, release_version)
    File.open(swift_environment_path, 'w') { |f| f.puts(new_content) }

    # Update sdk sizes
    Dir.chdir('fastlane') { update_img_shields_sdk_sizes }
  end

  match_me
  pod_lint
  release_ios_sdk(
    version: options[:version],
    bump_type: options[:type],
    sdk_names: sdk_names,
    podspec_names: ['StreamChat', 'StreamChat-XCFramework', 'StreamChatUI', 'StreamChatUI-XCFramework'],
    github_repo: github_repo,
    extra_changes: extra_changes,
    create_pull_request: true
  )
end

lane :merge_release do |options|
  merge_release_to_main(author: options[:author])
  sh('gh workflow run release-publish.yml --ref main')
end

lane :merge_main do
  merge_main_to_develop
  update_release_version_to_snapshot(file_path: swift_environment_path)
  ensure_git_branch(branch: 'develop')
  sh("git add #{swift_environment_path}")
  sh("git commit -m 'Update release version to snapshot'")
  sh('git push')
end

desc 'Completes an SDK Release'
lane :publish_release do |options|
  release_version = get_sdk_version_from_environment
  UI.user_error!("Release #{release_version} has already been published.") if git_tag_exists(tag: release_version, remote: true)
  UI.user_error!('Release version cannot be empty') if release_version.to_s.empty?
  ensure_git_branch(branch: 'main')

  clean_products
  build_xcframeworks
  compress_frameworks
  clean_products

  publish_ios_sdk(
    skip_git_status_check: false,
    version: release_version,
    sdk_names: sdk_names,
    podspec_names: ['StreamChat', 'StreamChat-XCFramework', 'StreamChatUI', 'StreamChatUI-XCFramework'],
    github_repo: github_repo,
    upload_assets: ['Products/StreamChat.zip', 'Products/StreamChatUI.zip', 'Products/StreamChat-All.zip']
  )

  update_spm(version: release_version)
end

lane :get_sdk_version_from_environment do
  File.read(swift_environment_path).match(/String\s+=\s+"([\d.]+).*"/)[1]
end

desc 'Compresses the XCFrameworks into zip files'
lane :compress_frameworks do
  Dir.chdir('..') do
    FileUtils.cp('LICENSE', 'Products/LICENSE')
    Dir.chdir('Products') do
      sdk_names.each do |framework|
        sh("zip -r #{framework} ./#{framework}.xcframework ./LICENSE")
        sh("swift package compute-checksum #{framework}.zip")
      end
      sh('zip -r "StreamChat-All" ./*.xcframework ./LICENSE')
    end
  end
end

desc 'Cleans Products and DerivedData folders'
lane :clean_products do
  Dir.chdir('..') do
    ['*.xcframework', '*.bundle', '*.BCSymbolMaps', '*.dSYMs', 'LICENSE'].each do |f|
      sh("rm -rf Products/#{f}") # FileUtils.rm_rf does not work from Makefile, using sh instead
    end
  end
end

desc 'Update XCFrameworks and submit to the SPM repository'
private_lane :update_spm do |options|
  version = options[:version] || ''
  UI.user_error!('You need to pass the version of the release you want to obtain the changelog from') unless version.length > 0

  # Generate Checksums
  stream_chat_checksum = sh('swift package compute-checksum ../Products/StreamChat.zip').strip
  stream_chat_ui_checksum = sh('swift package compute-checksum ../Products/StreamChatUI.zip').strip

  initial_directory = Dir.pwd

  # Update SPM Repo
  spm_directory_name = 'StreamSPM'
  spm_directory = "../../#{spm_directory_name}"
  sh("git clone git@github.com:#{github_repo}-spm.git ../../#{spm_directory_name}")
  Dir.chdir(spm_directory)

  result = sh('basename `git rev-parse --show-toplevel`').strip
  UI.error("Not using #{spm_directory_name} repo") unless result.to_s == spm_directory_name

  file_lines = File.readlines('Package.swift')
  file_data = ''
  previous_module = ''

  file_lines.each do |line|
    formatted_line =
      case previous_module
      when 'StreamChat'
        line.gsub(/(checksum: ")[a-z0-9]+(")/, "\\1#{stream_chat_checksum}\\2")
      when "StreamChatUI"
        line.gsub(/(checksum: ")[a-z0-9]+(")/, "\\1#{stream_chat_ui_checksum}\\2")
      else
        line
      end

    url_pattern = %r{(releases/download/)[.0-9]+(/)}
    if line.match(url_pattern)
      formatted_line = line.gsub(url_pattern, "\\1#{version}\\2")
      previous_module = line.match(/([a-zA-Z]+).zip/).to_s.gsub(/.zip/, '')
    end

    file_data << formatted_line
  end

  # Write the new changes
  File.open('./Package.swift', 'w') { |file| file << file_data }

  # Update the repo
  sh('git add -A')
  sh("git commit -m 'Bump #{version}'")
  sh('git push')

  github_release = set_github_release(
    repository_name: "#{github_repo}-spm",
    api_token: ENV.fetch('GITHUB_TOKEN', nil),
    name: version,
    tag_name: version,
    commitish: 'main',
    description: "https://github.com/#{github_repo}/releases/tag/#{version}"
  )

  UI.message("Moving back to fastlane's directory - #{initial_directory}")
  Dir.chdir(initial_directory)

  # Clean Up
  sh("rm -rf #{spm_directory}")
  UI.success("New SPM release available: #{github_release['html_url']}")
  github_release['html_url']
end

private_lane :appstore_api_key do
  @appstore_api_key ||= app_store_connect_api_key(
    key_id: 'MT3PRT8TB7',
    issuer_id: '69a6de96-0738-47e3-e053-5b8c7c11a4d1',
    key_content: ENV.fetch('APPSTORE_API_KEY', nil),
    in_house: false
  )
end

lane :pod_lint do
  # We don't lint StreamChatUI.podspec since pod lints it against StreamChat's remote version instead of local one
  pod_lib_lint(podspec: 'StreamChat.podspec', allow_warnings: true)
end

desc "If `readonly: true` (by default), installs all Certs and Profiles necessary for development and ad-hoc.\nIf `readonly: false`, recreates all Profiles necessary for development and ad-hoc, updates them locally and remotely."
lane :match_me do |options|
  app_identifiers = [
    'io.getstream.StreamChat',
    'io.stream.StreamChatUI',
    'io.getstream.iOS.ChatDemoApp',
    'io.getstream.iOS.ChatDemoAppTwo',
    'io.getstream.iOS.ChatDemoApp.DemoAppPush',
    'io.getstream.iOS.iMessageClone',
    'io.getstream.iOS.SlackClone',
    'io.getstream.iOS.MessengerClone',
    'io.getstream.iOS.YouTubeClone',
    'io.getstream.iOS.DemoAppUIKit',
    'io.getstream.iOS.ChatDemoApp.DemoShare',
    'io.getstream.iOS.StreamChatMockServer',
    'io.getstream.iOS.StreamChatUITestsApp',
    'io.getstream.iOS.StreamChatUITestsAppUITests.xctrunner'
  ]
  custom_match(
    api_key: appstore_api_key,
    app_identifier: app_identifiers,
    readonly: options[:readonly],
    register_device: options[:register_device]
  )
end

desc 'Builds the latest version of Demo app and uploads it to TestFlight'
lane :uikit_testflight_build do |options|
  is_manual_upload = is_localhost || ENV['GITHUB_EVENT_NAME'] == 'workflow_dispatch'
  configuration = options[:configuration].to_s.empty? ? 'Release' : options[:configuration]

  match_me

  sdk_version = get_sdk_version_from_environment
  UI.important("[TestFlight] Uploading DemoApp version: #{sdk_version}")

  testflight_build(
    api_key: appstore_api_key,
    xcode_project: xcode_project,
    sdk_target: 'StreamChat',
    app_target: 'DemoApp',
    app_version: sdk_version,
    app_identifier: 'io.getstream.iOS.ChatDemoApp',
    configuration: configuration,
    extensions: ['DemoShare'],
    use_changelog: true,
    is_manual_upload: is_manual_upload
  )
end

desc 'Get next PR number from github to be used in CHANGELOG'
lane :get_next_issue_number do
  result = github_api(api_token: ENV.fetch('FASTLANE_GITHUB_TOKEN', nil), path: "/repos/#{github_repo}/issues")

  next_issue_number = result[:json][0]['number'] + 1
  next_issue_link = "[##{next_issue_number}](https://github.com/#{github_repo}/issues/#{next_issue_number})"

  clipboard(value: next_issue_link)

  UI.success("The next PR / Issue will have number: #{next_issue_number}")
  UI.success("So the next markdown link is: #{next_issue_link}")
  UI.success('Next markdown link is copied to your clipboard! ⬆️')
end

desc 'Runs tests in Debug config'
lane :test do |options|
  next unless is_check_required(sources: sources_matrix[:llc], force_check: @force_check)

  update_testplan_on_ci(path: 'Tests/StreamChatTests/StreamChatFlakyTests.xctestplan')

  scan(
    project: xcode_project,
    scheme: 'StreamChat',
    testplan: 'StreamChatFlakyTests',
    clean: is_localhost,
    derived_data_path: derived_data_path,
    cloned_source_packages_path: source_packages_path,
    devices: options[:device],
    number_of_retries: 5,
    build_for_testing: options[:build_for_testing],
    skip_build: options[:skip_build],
    xcargs: buildcache_xcargs
  )

  next if options[:build_for_testing]

  update_testplan_on_ci(path: 'Tests/StreamChatTests/StreamChatTestPlan.xctestplan')

  scan_options = {
    project: xcode_project,
    scheme: 'StreamChat',
    testplan: 'StreamChatTestPlan',
    derived_data_path: derived_data_path,
    cloned_source_packages_path: source_packages_path,
    devices: options[:device],
    skip_build: true,
    xcargs: buildcache_xcargs,
    number_of_retries: options[:cron] ? 3 : 2
  }

  scan(scan_options)

  slather
end

desc 'Starts Sinatra web server'
lane :start_sinatra do
  sh('bundle exec ruby sinatra.rb > sinatra_log.txt 2>&1 &')
end

desc 'Stops Sinatra web server'
lane :stop_sinatra do
  sh('lsof -t -i:4567 | xargs kill -9')
end

lane :build_test_app_and_frameworks do
  scan(
    project: xcode_project,
    scheme: 'StreamChatUITestsApp',
    testplan: 'StreamChatUITestsApp',
    result_bundle: true,
    derived_data_path: derived_data_path,
    cloned_source_packages_path: source_packages_path,
    clean: is_localhost,
    build_for_testing: true,
    xcargs: buildcache_xcargs
  )
end

lane :xcmetrics do |options|
  next unless is_check_required(sources: sources_matrix[:xcmetrics], force_check: @force_check)

  ['test_output/', 'metrics/', "../#{derived_data_path}/Build/Products"].each { |dir| FileUtils.remove_dir(dir, force: true) }

  match_me

  scan(
    project: xcode_project,
    scheme: 'StreamChatUITestsApp',
    testplan: 'Performance',
    result_bundle: true,
    derived_data_path: derived_data_path,
    cloned_source_packages_path: source_packages_path,
    clean: is_localhost,
    xcargs: buildcache_xcargs,
    sdk: 'iphoneos',
    skip_detect_devices: true,
    build_for_testing: true
  )

  firebase_error = ''
  xcodebuild_output = ''
  Dir.chdir("../#{derived_data_path}/Build/Products") do
    begin
      sh("zip -r MyTests.zip .")
      sh("gcloud firebase test ios run --test MyTests.zip --timeout 7m --results-dir test_output --device 'model=iphone14pro,version=16.6,orientation=portrait'")
    rescue StandardError => e
      UI.error("Test failed on Firebase:\n#{e}")
      firebase_error = e
    end

    sh("gsutil cp -r #{testlab_bucket}/test_output/iphone14pro-16.6-en-portrait/xcodebuild_output.log xcodebuild_output.log")
    xcodebuild_output = File.read('xcodebuild_output.log')
  end

  warning_status = '🟡' # Warning if a branch is #{max_tolerance} less performant than the benchmark
  fail_status = '🔴' # Failure if a branch is more than #{max_tolerance} less performant than the benchmark
  success_status = '🟢' # Success if a branch is more performant or equals to the benchmark

  sh("git clone #{metrics_git} #{File.dirname(xcmetrics_path)}")
  performance_benchmarks = JSON.parse(File.read(xcmetrics_path))
  expected_performance = performance_benchmarks['benchmark']
  actual_performance = xcmetrics_log_parser(log: xcodebuild_output)

  table_header = '## SDK Performance'
  markdown_table = "#{table_header}\n| `target` | `metric` | `benchmark` | `branch` | `performance` | `status` |\n| - | - | - | - | - | - |\n"
  ['testMessageListScrollTime', 'testChannelListScrollTime'].each do |test_name|
    next if test_name == 'testChannelListScrollTime' # Delete this line and return the test to the testplan as soon as PBE-5666 is solved

    index = 0
    ['hitches_total_duration', 'duration', 'hitch_time_ratio', 'frame_rate', 'number_of_hitches'].each do |metric|
      is_frame_rate = metric == 'frame_rate'
      benchmark_value = expected_performance[test_name][metric]['value']
      branch_value = actual_performance[test_name][metric]['value']
      value_extension = actual_performance[test_name][metric]['ext']
      max_tolerance = benchmark_value * 0.1 # Default Xcode Max Tolerance is 10%

      benchmark_value_avoids_zero_division = benchmark_value == 0 ? 1 : benchmark_value
      diff = is_frame_rate ? branch_value - benchmark_value : benchmark_value - branch_value
      diff = (diff * 100.0 / benchmark_value_avoids_zero_division).round(2)
      diff_emoji = if diff > 0
                     '🔼'
                   elsif diff.zero?
                     '🟰'
                   else
                     '🔽'
                   end

      status_emoji =
        if is_frame_rate
          if branch_value < benchmark_value && branch_value > benchmark_value - max_tolerance
            warning_status
          elsif branch_value < benchmark_value
            fail_status
          else
            success_status
          end
        else
          if branch_value > benchmark_value && branch_value < benchmark_value + max_tolerance
            warning_status
          elsif branch_value > benchmark_value
            fail_status
          else
            success_status
          end
        end

      title = metric.to_s.gsub('_', ' ').capitalize
      target = index.zero? ? test_name.match(/(?<=test)(.*?)(?=ScrollTime)/).to_s : ''
      index += 1

      markdown_table << "| #{target} | #{title} | #{benchmark_value} #{value_extension} | #{branch_value} #{value_extension} | #{diff}% #{diff_emoji} | #{status_emoji} |\n"
      FastlaneCore::PrintTable.print_values(
        title: title,
        config: {
          benchmark: "#{benchmark_value} #{value_extension}",
          branch: "#{branch_value} #{value_extension}",
          diff: "#{diff}% #{diff_emoji}",
          status: status_emoji
        }
      )
    end
  end

  UI.user_error!("See Firebase error above ☝️") unless firebase_error.to_s.empty?

  pr_comment(text: markdown_table, edit_last_comment_with_text: table_header) if is_ci

  UI.user_error!("#{table_header} benchmark failed.") if markdown_table.include?(fail_status)
end

private_lane :xcmetrics_log_parser do |options|
  log = options[:log]
  method = 'Scroll_DraggingAndDeceleration'
  metrics = {}

  ['testMessageListScrollTime', 'testChannelListScrollTime'].each do |test_name|
    next if test_name == 'testChannelListScrollTime' # Delete this line and return the test to the testplan as soon as PBE-5666 is solved

    hitches_total_duration = log.match(/#{test_name}\]' measured \[Hitches Total Duration \(#{method}\), ms\] average: (\d+\.\d+)/)
    UI.user_error!("Hitches Total Duration not found for #{test_name}") if hitches_total_duration.nil?

    duration = log.match(/#{test_name}\]' measured \[Duration \(#{method}\), s\] average: (\d+\.\d+)/)
    UI.user_error!("Duration not found for #{test_name}") if duration.nil?

    hitch_time_ratio = log.match(/#{test_name}\]' measured \[Hitch Time Ratio \(#{method}\), ms per s\] average: (\d+\.\d+)/)
    UI.user_error!("Hitch Time Ratio not found for #{test_name}") if hitch_time_ratio.nil?

    frame_rate = log.match(/#{test_name}\]' measured \[Frame Rate \(#{method}\), fps\] average: (\d+\.\d+)/)
    UI.user_error!("Frame Rate not found for #{test_name}") if frame_rate.nil?

    number_of_hitches = log.match(/#{test_name}\]' measured \[Number of Hitches \(#{method}\), hitches\] average: (\d+\.\d+)/)
    UI.user_error!("Number of Hitches not found for #{test_name}") if number_of_hitches.nil?

    metrics[test_name] = {
      'hitches_total_duration' => {
        'value' => hitches_total_duration[1].to_f.round(2),
        'ext' => 'ms'
      },
      'duration' => {
        'value' => duration[1].to_f.round(2),
        'ext' => 's'
      },
      'hitch_time_ratio' => {
        'value' => hitch_time_ratio[1].to_f.round(2),
        'ext' => 'ms per s'
      },
      'frame_rate' => {
        'value' => frame_rate[1].to_f.round(2),
        'ext' => 'fps'
      },
      'number_of_hitches' => {
        'value' => number_of_hitches[1].to_f.round(2),
        'ext' => ''
      }
    }
  end

  metrics
end

desc 'Runs e2e ui tests using real backend server in Debug config'
lane :test_e2e do |options|
  scan_options = {
    project: xcode_project,
    scheme: 'StreamChatUITestsApp',
    testplan: 'Backend',
    result_bundle: true,
    derived_data_path: derived_data_path,
    cloned_source_packages_path: source_packages_path,
    clean: is_localhost,
    test_without_building: options[:test_without_building],
    xcargs: buildcache_xcargs,
    devices: options[:device],
    prelaunch_simulator: is_ci,
    number_of_retries: 3
  }

  begin
    scan(scan_options)
  rescue StandardError
    failed_tests = retreive_failed_tests
    UI.important("Re-running #{failed_tests.size} failed tests ⌛️")
    scan(scan_options.merge(only_testing: failed_tests))
  end
end

desc 'Runs e2e ui tests using mock server in Debug config'
lane :test_e2e_mock do |options|
  next unless is_check_required(sources: sources_matrix[:e2e], force_check: @force_check)

  start_sinatra

  scan_options = {
    project: xcode_project,
    scheme: 'StreamChatUITestsApp',
    testplan: 'StreamChatUITestsApp',
    result_bundle: true,
    derived_data_path: derived_data_path,
    cloned_source_packages_path: source_packages_path,
    clean: is_localhost,
    test_without_building: options[:test_without_building],
    xcargs: buildcache_xcargs,
    devices: options[:device],
    prelaunch_simulator: is_ci,
    number_of_retries: 3
  }

  if ENV['MATRIX_SIZE'] && options[:batch]
    products_dir = File.expand_path("../#{derived_data_path}/Build/Products")
    xctestrun = Dir.glob(File.expand_path("#{products_dir}/*.xctestrun")).first
    tests = retrieve_xctest_names(xctestrun: xctestrun).values.flatten
    slice_size = (tests.size / ENV['MATRIX_SIZE'].to_f).ceil
    only_testing = []
    tests.each_slice(slice_size) { |test| only_testing << test }
    only_testing_batch = only_testing[options[:batch].to_i]
    scan_options[:only_testing] = only_testing_batch
    UI.important("Tests in total: #{only_testing.flatten.size}. Running #{only_testing_batch.size} of them ⌛️")
  end

  begin
    scan(scan_options)
  rescue StandardError
    failed_tests = retreive_failed_tests
    UI.important("Re-running #{failed_tests.size} failed tests ⌛️")
    scan(scan_options.merge(only_testing: failed_tests))
  end
end

private_lane :retreive_failed_tests do
  report_path = 'test_output/report.junit'
  raise UI.user_error!('There is no junit report to parse') unless File.file?(report_path)

  junit_report = Nokogiri::XML(File.read(report_path))
  failed_tests = []
  passed_tests = []
  suite_name = junit_report.xpath('//testsuite').first['name'].split('.').first
  junit_report.xpath('//testcase').each do |testcase|
    class_name = testcase['classname'].split('.').last
    test_name = testcase['name'].delete('()')

    if testcase.at_xpath('failure')
      failed_tests << "#{suite_name}/#{class_name}/#{test_name}"
    else
      passed_tests << "#{suite_name}/#{class_name}/#{test_name}"
    end
  end

  (failed_tests - passed_tests).uniq
end

desc 'Runs ui tests in Debug config'
lane :test_ui do |options|
  next unless is_check_required(sources: sources_matrix[:ui], force_check: @force_check)

  record_mode = options[:record].to_s == 'true'
  remove_snapshots if record_mode

  update_testplan_on_ci(path: 'Tests/StreamChatUITests/StreamChatUITestPlan.xctestplan')

  scan(
    project: xcode_project,
    scheme: 'StreamChatUI',
    testplan: 'StreamChatUITestPlan',
    clean: is_localhost,
    derived_data_path: derived_data_path,
    cloned_source_packages_path: source_packages_path,
    build_for_testing: options[:build_for_testing],
    skip_build: options[:skip_build],
    result_bundle: true,
    devices: options[:device],
    xcargs: buildcache_xcargs,
    fail_build: !record_mode
  )

  if record_mode && is_ci
    png_files = git_status(ext: '.png').map { |_, png| png }.flatten
    next if png_files.empty?

    # Discard all files apart from the snapshots
    Dir.chdir('..') do
      png_files.each { |png| sh("git add #{png}") || true }
      sh('git restore .')
    end

    pr_create(
      title: '[CI] Snapshots',
      base_branch: current_branch,
      head_branch: "#{current_branch}-snapshots-#{Time.now.to_i}"
    )
  end
end

private_lane :match_macos do
  %w[development appstore].each do |type|
    match(
      type: type,
      app_identifier: 'io.getstream.StreamChat',
      platform: 'macos'
    )
  end
end

desc 'Runs stress tests for Debug config'
lane :stress_test do
  scan(
    project: xcode_project,
    scheme: 'StreamChat',
    clean: true,
    build_for_testing: true,
    devices: options[:device]
  )

  update_testplan_on_ci(path: 'Tests/StreamChatTests/StreamChatStressTestPlan.xctestplan')

  stress_tests_cycles.times do
    scan(
      project: xcode_project,
      scheme: 'StreamChat',
      test_without_building: true,
      testplan: 'StreamChatStressTestPlan',
      devices: options[:device],
      xcpretty_args: '--test' # simplify logs
    )
  end
end

desc 'Builds Demo app'
lane :build_demo do |options|
  options[:scheme] = 'DemoApp'
  build_example_app(options)
end

desc 'Builds iMessageClone app'
lane :build_imessage_clone do |options|
  options[:scheme] = 'iMessage'
  build_example_app(options)
end

desc 'Builds SlackClone app'
lane :build_slack_clone do |options|
  options[:scheme] = 'Slack'
  build_example_app(options)
end

desc 'Builds MessengerClone app'
lane :build_messenger_clone do |options|
  options[:scheme] = 'Messenger'
  build_example_app(options)
end

desc 'Builds YouTubeClone app'
lane :build_youtube_clone do |options|
  options[:scheme] = 'YouTube'
  build_example_app(options)
end

private_lane :build_example_app do |options|
  next unless is_check_required(sources: sources_matrix[:sample_apps], force_check: @force_check)

  scan(
    project: xcode_project,
    scheme: options[:scheme],
    clean: is_localhost,
    derived_data_path: derived_data_path,
    cloned_source_packages_path: source_packages_path,
    build_for_testing: true,
    devices: options[:device],
    xcargs: buildcache_xcargs
  )
end

desc 'Test SPM Integration'
lane :spm_integration do
  next unless is_check_required(sources: sources_matrix[:integration], force_check: @force_check)

  gym(
    project: 'Integration/SPM/SwiftPackageManager.xcodeproj',
    scheme: 'SwiftPackageManager',
    skip_package_ipa: true,
    skip_archive: true,
    clean: is_localhost,
    derived_data_path: derived_data_path,
    cloned_source_packages_path: source_packages_path,
    destination: 'generic/platform=iOS Simulator',
    xcargs: buildcache_xcargs
  )
end

desc 'Test CocoaPods Integration'
lane :cocoapods_integration do
  next unless is_check_required(sources: sources_matrix[:integration], force_check: @force_check)

  cocoapods(
    clean_install: true,
    podfile: 'Integration/CocoaPods/'
  )

  gym(
    workspace: 'Integration/CocoaPods/CocoaPods.xcworkspace',
    scheme: 'CocoaPods',
    skip_package_ipa: true,
    skip_archive: true,
    clean: is_localhost,
    derived_data_path: derived_data_path,
    cloned_source_packages_path: source_packages_path,
    destination: 'generic/platform=iOS Simulator',
    xcargs: buildcache_xcargs
  )
end

private_lane :update_testplan_on_ci do |options|
  update_testplan(path: options[:path], env_vars: { key: 'CI', value: 'TRUE' }) if is_ci
end

lane :sync_mock_server do
  sh('bundle exec ruby sync_mock_server.rb')
  next unless is_ci

  pr_create(
    title: '[CI] Sync Mock Server',
    head_branch: "ci/sync-mock-server-#{Time.now.to_i}",
    git_add: 'TestTools/StreamChatTestMockServer/Fixtures/'
  )
end

desc 'Run fastlane linting'
lane :rubocop do
  next unless is_check_required(sources: sources_matrix[:ruby], force_check: @force_check)

  sh('bundle exec rubocop')
end

desc 'Run PR linting'
lane :lint_pr do
  danger(dangerfile: 'Dangerfile') if is_ci
end

desc 'Run source code formatting/linting'
lane :run_swift_format do |options|
  Dir.chdir('..') do
    strict = options[:strict] ? '--lint' : nil
    sh("swiftformat #{strict} --config .swiftformat .")
    sh("swiftlint lint --config .swiftlint.yml --fix --progress --reporter json") unless strict
    sh("swiftlint lint --config .swiftlint.yml --strict --progress --reporter json")
  end
end

lane :install_runtime do |options|
  install_ios_runtime(version: options[:ios], custom_script: 'Scripts/install_ios_runtime.sh')
end

desc 'Remove UI snapshots'
private_lane :remove_snapshots do |options|
  snapshots_path = "../Tests/StreamChatUITests/**/__Snapshots__/**/*.png"
  if options[:only_unchanged]
    pnf_files = git_status(ext: '.png')
    changed_snapshots = (pnf_files[:a] + pnf_files[:m]).map { |f| File.expand_path(f) }
    Dir.glob(snapshots_path).select { |f| File.delete(f) unless changed_snapshots.include?(File.expand_path(f)) }
  else
    Dir.glob(snapshots_path).select { |f| File.delete(f) }
  end
end

lane :sources_matrix do
  {
    e2e: ['Sources', 'StreamChatUITestsAppUITests', 'StreamChatUITestsApp', 'TestTools/StreamChatTestMockServer', xcode_project],
    llc: ['Sources/StreamChat', 'Tests/StreamChatTests', 'Tests/Shared', 'TestTools/StreamChatTestTools', xcode_project],
    ui: ['Sources', 'Tests/StreamChatUITests', 'Tests/Shared', xcode_project],
    sample_apps: ['Sources', 'Examples', 'DemoApp', xcode_project],
    integration: ['Sources', 'Integration', xcode_project],
    ruby: ['fastlane', 'Gemfile', 'Gemfile.lock'],
    size: ['Sources', xcode_project],
    xcmetrics: ['Sources'],
    public_interface: ['Sources']
  }
end

lane :copyright do
  update_copyright(ignore: [derived_data_path, source_packages_path, 'vendor/'])
  next unless is_ci

  pr_create(
    title: '[CI] Update Copyright',
    head_branch: "ci/update-copyright-#{Time.now.to_i}"
  )
end

lane :validate_public_interface do
  next unless is_check_required(sources: sources_matrix[:public_interface], force_check: @force_check)

  # Get branch names
  original_branch = current_branch
  target_branch = ENV['GITHUB_BASE_REF'] || (original_branch.include?('release/') ? 'main' : 'develop')
  UI.important("Target branch: #{target_branch} 🕊️")

  # Run the analysis on the current branch
  sh('interface-analyser analysis ../Sources/ public_interface_current.json')

  # Checkout the target branch
  sh("git fetch origin #{target_branch}")
  sh("git checkout #{target_branch}")

  # Run the analysis on the target branch
  sh('interface-analyser analysis ../Sources/ public_interface_previous.json')

  # Run diff
  report_path = 'interface-analyser-report.md'
  sh("interface-analyser diff public_interface_current.json public_interface_previous.json #{report_path}")

  # Check if report exists and is non-zero in size
  diff =
    if File.exist?(report_path) && File.size(report_path) > 0
      File.read(report_path).strip
    else
      '🚀 No changes affecting the public interface.'
    end

  # Generate markdown table for the PR comment
  header = '## Public Interface'
  content = "#{header}\n#{diff}"

  # Post PR comment if running in CI
  pr_comment(text: content, edit_last_comment_with_text: header) if is_ci

  # Checkout the original branch
  sh("git fetch origin #{original_branch}")
  sh("git checkout #{original_branch}")
end

lane :show_frameworks_sizes do |options|
  next unless is_check_required(sources: sources_matrix[:size], force_check: @force_check)

  sizes = options[:sizes] || frameworks_sizes
  show_sdk_size(branch_sizes: sizes, github_repo: github_repo)
  update_img_shields_sdk_sizes(sizes: sizes, open_pr: options[:open_pr]) if options[:update_readme]
end

lane :update_img_shields_sdk_sizes do |options|
  update_sdk_size_in_readme(
    open_pr: options[:open_pr] || false,
    readme_path: 'README.md',
    sizes: options[:sizes] || frameworks_sizes
  )
end

private_lane :frameworks_sizes do
  root_dir = 'Build/SDKSize'
  archive_dir = "#{root_dir}/DemoApp.xcarchive"

  # Cleanup the previous builds
  FileUtils.rm_rf("../#{root_dir}/")

  match_me

  gym(
    scheme: 'DemoApp',
    archive_path: archive_dir,
    export_method: 'ad-hoc',
    export_options: 'fastlane/sdk_size_export_options.plist',
    derived_data_path: derived_data_path,
    cloned_source_packages_path: source_packages_path
  )

  # Parse the thinned size of Assets.car from Packaging.log
  assets_size_regex = %r{\b(\d+)\sbytes\sfor\s./Payload/ChatSample.app/Frameworks/StreamChatUI.framework/Assets.car\b}
  packaging_log_content = File.read("#{Gym.cache[:temporary_output_path]}/Packaging.log")
  match = packaging_log_content.match(assets_size_regex)
  assets_thinned_size = match[1].to_i

  frameworks_path = "../#{archive_dir}/Products/Library/Frameworks/ChatSample.app/Frameworks"
  stream_chat_size = File.size("#{frameworks_path}/StreamChat.framework/StreamChat")
  stream_chat_ui_size = File.size("#{frameworks_path}/StreamChatUI.framework/StreamChatUI")

  stream_chat_size_kb = stream_chat_size / 1024.0
  stream_chat_ui_size_kb = (stream_chat_ui_size + assets_thinned_size) / 1024.0

  {
    StreamChat: stream_chat_size_kb.round(0),
    StreamChatUI: stream_chat_ui_size_kb.round(0)
  }
end

lane :size_analyze do
  next unless is_check_required(sources: sources_matrix[:size], force_check: @force_check)

  gym(
    scheme: 'DemoApp',
    configuration: 'Release',
    skip_archive: true,
    skip_package_ipa: true,
    skip_package_pkg: true,
    skip_codesigning: true,
    derived_data_path: derived_data_path,
    cloned_source_packages_path: source_packages_path
  )

  show_detailed_sdk_size(sdk_names: sdk_names, threshold: 42)
end
